﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace FileEncoding.Helper
{
    public static class SmartLoad
    {
        // consts
        internal const int LCMAP_SIMPLIFIED_CHINESE = 0x02000000;
        internal const int LCMAP_TRADITIONAL_CHINESE = 0x04000000;

        // APIs

        [DllImport("kernel32.dll", EntryPoint = "LCMapStringA")]
        static extern int LCMapStringA(uint Locale, uint dwMapFlags,
            byte[] lpSrcStr, int cchSrc, byte[] lpDestStr, int cchDest);

        [DllImport("kernel32.dll")]
        static extern uint GetUserDefaultLCID();

        [DllImport("kernel32.dll")]
        static extern int MultiByteToWideChar(uint CodePage, uint dwFlags,
            [MarshalAs(UnmanagedType.LPArray)] byte[] lpMultiByteStr,
           int cbMultiByte, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpWideCharStr, int cchWideChar);

        [DllImport("kernel32.dll")]
        static extern int WideCharToMultiByte(uint CodePage, uint dwFlags,
           [MarshalAs(UnmanagedType.LPWStr)] string lpWideCharStr, int cchWideChar,
           [MarshalAs(UnmanagedType.LPArray)] Byte[] lpMultiByteStr, int cbMultiByte, IntPtr lpDefaultChar,
           IntPtr lpUsedDefaultChar);

        /*********************************************************************
         * GB & Big5 Convertion
         * 貌似以下方法可以在简体系统下正常将简体转为繁体并存成繁体编码文件
         * 但不知道在繁体系统下可否正常工作？！ 2013.1.31~2013.2.18
         *********************************************************************/

        private static string UnicodeEncode(byte[] source, uint CodePage)
        {
            StringBuilder s = new StringBuilder(source.Length);
            int len = MultiByteToWideChar(CodePage, 0, source, -1, s, s.Capacity);
            return s.ToString();
        }

        private static byte[] UnicodeDecode(string source, uint CodePage)
        {
            byte[] ret = new byte[source.Length * 2 + 1];
            int len = WideCharToMultiByte(CodePage, 0, source, -1, ret, ret.Length, IntPtr.Zero, IntPtr.Zero);
            len--;
            byte[] ok = new byte[len];
            Array.Copy(ret, ok, len);
            return ok;
        }


        public static byte[] Gb2Big5(byte[] source)
        {
            byte[] result = new byte[source.Length];
            LCMapStringA(GetUserDefaultLCID(), LCMAP_TRADITIONAL_CHINESE, source, source.Length, result, result.Length);
            return UnicodeDecode(UnicodeEncode(result, 936), 950);
        }

        public static byte[] Big52Gb(byte[] source)
        {
            byte[] tmp = UnicodeDecode(UnicodeEncode(source, 950), 936);
            byte[] result = new byte[tmp.Length];
            LCMapStringA(GetUserDefaultLCID(), LCMAP_SIMPLIFIED_CHINESE, tmp, tmp.Length, result, result.Length);
            return result;
        }

        /*********************************************************************
         * Smart Load
         *********************************************************************/

        public static bool SysIsSimplifiedChinese()
        {
            return "zh-CN".Equals(System.Threading.Thread.CurrentThread.CurrentCulture.Name);
        }

        public static bool SysIsTraditionalChinese()
        {
            return "zh-TW".Equals(System.Threading.Thread.CurrentThread.CurrentCulture.Name);
        }

        /*-------------------------------------------------------------------*/

        public static int SamplingSize = 4096;
        public static bool ExactCompare = false;

        public enum CharEncoding { Ansi, Binary, Utf_8, Ucs2_LE, Ucs2_BE, Utf32_LE, Utf32_BE, GB, Big5 };

        /*-------------------------------------------------------------------*/

        private static int CountChineseChars(byte[] buf)
        {
            int count = 0;
            int i = 0;
            while (i < buf.Length)
            {
                if (buf[i] > 0x80)
                {
                    count += 2;
                    i += 2;
                }
                else
                {
                    i++;
                }
            }
            return count;
        }

        private static bool MyCompareChineseStr(byte[] s1, byte[] s2)
        {
            int difCount = 0;
            int i = 0;
            int k = 0;
            while (i < s1.Length && k < s2.Length)
            {
                if (s1[i] != s2[k])
                {
                    if (i + 2 < s1.Length && k + 2 < s2.Length)
                    {
                        //比较原理：两次转换后，某些字可能会转戌一个'?'，以下处理这种情况
                        if (s1[i + 1] == s2[k] && s1[i + 2] == s2[k + 1])
                        {
                            i++;
                        }
                        else if (s1[i] == s2[k + 1] && s1[i + 1] == s2[k + 2])
                        {
                            k++;
                        }
                        else
                        {
                            difCount++;
                        }
                    }
                    else
                    {
                        difCount++;
                    }
                }
                i++;
                k++;
            }
            int cmpLen = ExactCompare ? CountChineseChars(s1) : s1.Length;
            return difCount * 100 / Math.Max(1, cmpLen) <= 6; // different <= 6%            
        }

        /*-------------------------------------------------------------------*/

        public static CharEncoding TryToDistinguishGBOrBig5(byte[] source)
        {
            if (SysIsSimplifiedChinese())
            {
                return MyCompareChineseStr(source, Big52Gb(Gb2Big5(source))) ? CharEncoding.GB : CharEncoding.Big5;
            }
            else if (SysIsTraditionalChinese())
            {
                return MyCompareChineseStr(source, Gb2Big5(Big52Gb(source))) ? CharEncoding.Big5 : CharEncoding.GB;
            }
            return CharEncoding.Ansi;
        }

        private static bool Maybe3BytesUtf8(byte[] buf, int len, int index)
        {
            /*
            return index +2 < len && buf[index] >= 0xE0 && buf[index] <= 0xEF &&
                buf[index +1] >= 0x80 && buf[index +1] <= 0xBF &&
                buf[index +2] >= 0x80 && buf[index +2] <= 0xBF;
             */
            return index + 2 < len && (buf[index] & 0xF0) == 0xE0 &&
                (buf[index + 1] & 0xC0) == 0x80 && (buf[index + 2] & 0xC0) == 0x80;
        }

        public static CharEncoding GuessCharEncoding(byte[] buf, bool seeGBBig5)
        {
            int idx = 0;
            int maybeGB = 0;
            int mayBig5 = 0;
            int mayUtf8 = 0;
            int maybeLE = 0;
            int maybeBE = 0;
            int mayBins = 0;
            int serZero = 0;
            int chsCount = 0;
            int utf8Count = 0;
            int gbkNulls = 0;
            int big5Nulls = 0;
            int len = buf.Length;

            while (idx < len)
            {
                if (idx + 1 < len)
                {
                    if (buf[idx] >= 0xA1 && buf[idx] <= 0xA7 && buf[idx + 1] >= 0x40 && buf[idx + 1] <= 0xA0 ||
                        (buf[idx] >= 0xAA && buf[idx] <= 0xAF || buf[idx] >= 0xF8 && buf[idx] <= 0xFE) &&
                            buf[idx + 1] >= 0xA1 && buf[idx + 1] <= 0xFE)
                    {
                        gbkNulls++;
                    }
                    if (buf[idx + 1] >= 0x7F && buf[idx + 1] <= 0xA0 ||
                        buf[idx] >= 0xC7 && buf[idx] <= 0xC8 && buf[idx + 1] >= 0x40 && buf[idx + 1] <= 0xFE ||
                        buf[idx] == 0xC6 && buf[idx + 1] >= 0xA1 && buf[idx + 1] <= 0xFE)
                    {
                        big5Nulls++;
                    }
                }
                if (buf[idx] == 0x00)
                {
                    mayBins++;
                    if (idx + 1 < len && buf[idx + 1] == 0x00)
                    {
                        serZero++;
                    }
                    if (idx % 2 == 1)
                    {
                        maybeLE++;
                    }
                    else
                    {
                        maybeBE++;
                    }
                }
                else if (buf[idx] >= 1 && buf[idx] <= 8 || buf[idx] == 11 || buf[idx] == 12 ||
                    buf[idx] >= 14 && buf[idx] <= 31)
                {
                    mayBins++;
                }
                else if (buf[idx] == 0x80)
                {
                    int iUtf8 = idx;
                    iUtf8++;
                    if (iUtf8 + 1 < len && buf[iUtf8] >= 0x80 && buf[iUtf8] <= 0xBF) iUtf8++;
                    if (Maybe3BytesUtf8(buf, len, iUtf8))
                    {
                        mayUtf8 += 32;
                        utf8Count++;
                    }
                }
                else if (buf[idx] >= 0x81 && buf[idx] <= 0xBF)
                {
                    if (buf[idx] >= 0x81 && buf[idx] <= 0xA0)
                    {
                        maybeGB += 8;
                    }
                    else
                    {
                        maybeGB += 8;
                        mayBig5 += 8;
                    }
                    chsCount++;
                    idx++;
                    int iUtf8 = idx;
                    if (iUtf8 + 1 < len && buf[iUtf8] >= 0x80 && buf[iUtf8] <= 0xBF) iUtf8++;
                    if (Maybe3BytesUtf8(buf, len, iUtf8))
                    {
                        mayUtf8 += 32;
                        utf8Count++;
                    }
                }
                else if (buf[idx] >= 0xC0 && buf[idx] <= 0xDF)
                {
                    if (idx + 1 < len && buf[idx + 1] >= 0x80 && buf[idx + 1] <= 0xBF)
                    {
                        mayUtf8++;
                        utf8Count++;
                        chsCount++;
                        if (buf[idx + 1] >= 0xA1 && buf[idx + 1] <= 0xBF)
                        {
                            maybeGB++;
                            mayBig5++;
                        }
                        else
                        {
                            maybeGB += 4;
                        }
                    }
                    else
                    {
                        maybeGB++;
                        mayBig5++;
                        chsCount++;
                    }
                    idx++;
                }
                else if (buf[idx] >= 0xE0 && buf[idx] <= 0xEF)
                {
                    if (idx + 2 < len && buf[idx + 1] >= 0x80 && buf[idx + 1] <= 0xBF && buf[idx + 2] >= 0x80 && buf[idx + 2] <= 0xBF)
                    {
                        mayUtf8 += 32;
                        utf8Count++;
                    }
                    maybeGB++;
                    mayBig5++;
                    chsCount++;
                    idx++;
                }
                else if (buf[idx] >= 0xF0 && buf[idx] <= 0xFE)
                {
                    if (buf[idx] >= 0xFA && buf[idx] <= 0xFE) maybeGB += 8;
                    maybeGB += 8;
                    mayBig5 += 8;
                    chsCount++;
                    idx++;
                }
                idx++;
            }

            // set encoding
            if (mayBins > 1 || maybeLE > 1 || maybeBE > 1 || mayBins * 8 >= len ||
                maybeLE * 8 >= len || maybeBE * 8 >= len)
            {
                if (mayBins > maybeLE * 2 && mayBins > maybeBE * 2 || serZero > 10 ||
                    serZero * 8 > Math.Max(maybeBE, maybeLE))
                {
                    return CharEncoding.Binary;
                }
                return (maybeLE >= maybeBE) ? CharEncoding.Ucs2_LE : CharEncoding.Ucs2_BE;
            }
            else if (maybeGB >= mayUtf8 || mayBig5 >= mayUtf8 || chsCount >= utf8Count * 2)
            {
                int ratio = (maybeGB - mayBig5) * 100 / Math.Max(1, Math.Max(maybeGB, mayBig5));
                if (Math.Abs(ratio) <= 5)
                {
                    if (gbkNulls > big5Nulls)
                    {
                        return CharEncoding.Big5;
                    }
                    else if (gbkNulls < big5Nulls)
                    {
                        return CharEncoding.GB;
                    }
                    else if (seeGBBig5 && (maybeGB > 0 || mayBig5 > 0))
                    {
                        return TryToDistinguishGBOrBig5(buf);
                    }
                }
                else
                {
                    return ratio > 0 ? CharEncoding.GB : CharEncoding.Big5;
                }
            }
            else if (mayUtf8 > 0)
            {
                return CharEncoding.Utf_8;
            }
            return CharEncoding.Ansi;
        }

        public static CharEncoding GuessCharEncoding(Stream aStream, bool seeGBBig5)
        {
            byte[] buf = new byte[Math.Min(SamplingSize, aStream.Length - aStream.Position)];
            aStream.Read(buf, 0, buf.Length);
            return GuessCharEncoding(buf, seeGBBig5);
        }

        /*-------------------------------------------------------------------*/

        private static bool SameBytes(byte[] buf, int count, params byte[] vals)
        {
            for (int i = 0; i < count; i++)
            {
                if (buf[i] != vals[i]) return false;
            }
            return true;
        }

        private static CharEncoding DoTryToDecode(Stream source, out StreamReader target, bool seeGBBig5)
        {
            CharEncoding result = GuessCharEncoding(source, seeGBBig5);
            source.Seek(0, SeekOrigin.Begin);
            switch (result)
            {
                case CharEncoding.Utf_8:
                    target = new StreamReader(source, Encoding.UTF8);
                    break;
                case CharEncoding.Ucs2_BE:
                    target = new StreamReader(source, Encoding.BigEndianUnicode);
                    break;
                case CharEncoding.Ucs2_LE:
                    target = new StreamReader(source, Encoding.Unicode);
                    break;
                case CharEncoding.Utf32_BE:
                    target = new StreamReader(source, new UTF32Encoding(true, true));
                    break;
                case CharEncoding.Utf32_LE:
                    target = new StreamReader(source, new UTF32Encoding(false, true));
                    break;
                case CharEncoding.GB:
                    target = new StreamReader(source, Encoding.GetEncoding("GB18030"));
                    break;
                case CharEncoding.Big5:
                    target = new StreamReader(source, Encoding.GetEncoding("Big5"));
                    break;
                default:
                    target = new StreamReader(source, Encoding.Default);
                    break;
            }
            return result;
        }

        public static CharEncoding SmartLoadTextFileToStream(string fileName,
            bool tryToDecode, bool seeGBBig5, out StreamReader aStreamReader)
        {
            aStreamReader = null;
            FileStream fs = File.OpenRead(fileName);
            if (fs.CanRead)
            {
                byte[] bom = new byte[4];
                int reads = fs.Read(bom, 0, 4);
                if (reads >= 2)
                {
                    fs.Seek(0, SeekOrigin.Begin);
                    if (reads >= 4)
                    {
                        //UTF32, Big-Endian
                        if (SameBytes(bom, 4, 0x00, 0x00, 0xFE, 0xFF))
                        {
                            aStreamReader = new StreamReader(fs, new UTF32Encoding(true, true));
                            return CharEncoding.Utf32_BE;
                        }
                        //UTF32, Little-Endian
                        if (SameBytes(bom, 4, 0xFF, 0xFE, 0x00, 0x00))
                        {
                            aStreamReader = new StreamReader(fs, new UTF32Encoding(false, true));
                            return CharEncoding.Utf32_LE;
                        }
                    }
                    //UTF8
                    if (reads >= 3 && SameBytes(bom, 3, 0xEF, 0xBB, 0xBF))
                    {
                        aStreamReader = new StreamReader(fs, Encoding.UTF8);
                        return CharEncoding.Utf_8;
                    }
                    //UTF16, Big-Endian
                    if (SameBytes(bom, 2, 0xFE, 0xFF))
                    {
                        aStreamReader = new StreamReader(fs, Encoding.BigEndianUnicode);
                        return CharEncoding.Ucs2_BE;
                    }
                    //UTF16, Little-Endian
                    if (SameBytes(bom, 2, 0xFF, 0xFE))
                    {
                        aStreamReader = new StreamReader(fs, Encoding.Unicode);
                        return CharEncoding.Ucs2_LE;
                    }
                    if (tryToDecode)
                    {
                        return DoTryToDecode(fs, out aStreamReader, seeGBBig5);
                    }
                }
            }
            fs.Seek(0, SeekOrigin.Begin);
            aStreamReader = new StreamReader(fs, Encoding.Default);
            return CharEncoding.Ansi;
        }
    }
}
